import subprocess
import re
import logging
import tempfile
import os
from pathlib import Path

import requests

from .exceptions import (BinaryAnalysisError, ExternalCommandError,
                         NetworkError, FileSystemError)
from .utils import run_command, download_file_with_progress

SYSTEM_LIB_PATHS = ('/lib', '/lib64', '/usr/lib', '/usr/lib64')

# 系统虚拟库，无需检查兼容性
VIRTUAL_LIBS = {'linux-vdso.so', 'ld-linux-x86-64.so', 'ld.so'}


def is_elf_file(path):
    """检查文件是否为ELF格式"""
    try:
        with open(path, 'rb') as file_handle:
            return file_handle.read(4) == b'\x7fELF'
    except (OSError, IOError) as error:
        logging.warning("无法读取文件 %s: %s", path, error)
        return False


def parse_symbols_from_readelf(file_path, symbol_type='required'):
    """
    统一的符号解析函数

    Args:
        file_path: 文件路径
        symbol_type: 'required' 获取未定义符号, 'provided' 获取已定义符号

    Returns:
        set: 符号集合（保留完整版本信息）
    """
    symbols = set()
    try:
        command = ['readelf', '-s', '--dyn-syms', '--wide', str(file_path)]
        result = run_command(command, stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)

        # readelf输出格式解析
        pattern = re.compile(
            r'^\s*\d+:\s+[0-9a-f]+\s+\d+\s+\w+\s+(GLOBAL|WEAK|LOCAL)\s+'
            r'(DEFAULT|HIDDEN|PROTECTED)\s+(\w+|\d+)\s+(\S+)')

        for line in result.stdout.decode('utf-8', errors='ignore').splitlines():
            match = pattern.match(line)
            if match:
                bind_type = match.group(1)
                visibility = match.group(2)
                ndx = match.group(3)
                symbol_name = match.group(4)

                # 只处理外部可见的符号
                if visibility == 'DEFAULT':
                    if symbol_type == 'required' and ndx == 'UND':
                        # 必需的符号必须是 GLOBAL。WEAK 符号是可选的，因此我们忽略它们。
                        if bind_type == 'GLOBAL':
                            # 保留完整的符号版本信息，不进行归一化
                            symbols.add(symbol_name)
                    elif symbol_type == 'provided' and ndx != 'UND':
                        # 提供的符号可以是 GLOBAL 或 WEAK
                        if bind_type in ['GLOBAL', 'WEAK']:
                            # 保留完整的符号版本信息，不进行归一化
                            symbols.add(symbol_name)

    except ExternalCommandError as error:
        logging.warning(
            "Failed to analyze symbols from %s: %s", file_path, error)

    return symbols


def analyze_binary_dependencies(binary_path):
    """分析二进制文件的运行时依赖"""
    logging.debug("[*] Analyzing runtime dependencies for binary: %s",
                  binary_path)

    if not binary_path.exists():
        raise BinaryAnalysisError(f"Binary file does not exist: {binary_path}")

    if not binary_path.is_file():
        raise BinaryAnalysisError(f"Path is not a file: {binary_path}")

    dependencies = {'system': set(), 'custom': set(), 'not_found': set()}

    try:
        command = ['ldd', str(binary_path.resolve())]
        result = run_command(command, stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)

        pattern = re.compile(
            r"\s*([a-zA-Z0-9_\-\+\.]+\.so[\.0-9]*)\s*=>\s*(.*)")
        for line in result.stdout.decode('utf-8', errors='ignore').splitlines():
            line = line.strip()

            # 跳过虚拟库
            if any(vlib in line for vlib in VIRTUAL_LIBS):
                continue

            match = pattern.match(line)
            if not match:
                continue
            lib_name = match.group(1)
            lib_path_info = match.group(2)
            if "not found" in lib_path_info:
                dependencies['not_found'].add(lib_name)
                continue
            lib_path_str = lib_path_info.split()[0]
            if lib_path_str.startswith(SYSTEM_LIB_PATHS):
                dependencies['system'].add(lib_name)
            else:
                dependencies['custom'].add(lib_name)

    except ExternalCommandError as error:
        if "statically linked" in str(error):
            logging.warning("Detected statically linked binary")
            return dependencies
        raise BinaryAnalysisError(
            f"Failed to analyze binary dependencies: {error}") from error

    logging.debug("[+] Analysis complete. Found %d system, %d custom, and %d missing dependencies.",
                  len(dependencies['system']), len(dependencies['custom']),
                  len(dependencies['not_found']))
    return dependencies


def get_required_symbols(binary_path):
    """获取二进制文件需要的符号"""
    logging.debug("[*] Analyzing required symbols from: %s", binary_path)

    if not binary_path.exists():
        raise BinaryAnalysisError(f"Binary file does not exist: {binary_path}")

    try:
        required = parse_symbols_from_readelf(binary_path, 'required')
        logging.debug("[+] Found %d required external symbols.", len(required))
        return required
    except Exception as error:
        raise BinaryAnalysisError(
            f"Failed to analyze symbols from {binary_path}: {error}") from error


def _extract_and_analyze_rpm(rpm_path, rpm_filename):
    """提取RPM并分析符号"""
    with tempfile.TemporaryDirectory(
            prefix=f"{rpm_filename}-extract-") as extract_dir_str:
        extract_dir = Path(extract_dir_str)
        try:
            command = f'rpm2cpio {rpm_path} | cpio -idmv'
            run_command(command, shell=True, cwd=extract_dir,
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except ExternalCommandError as error:
            raise ExternalCommandError(
                f"Failed to extract RPM: {error}") from error

        # 查找.so文件并分析符号
        so_files = []
        try:
            for root, _, files in os.walk(extract_dir):
                for file in files:
                    full_path = os.path.join(root, file)
                    if ((file.endswith('.so') or '.so.' in file) and
                            is_elf_file(full_path)):
                        so_files.append(full_path)
        except OSError as error:
            raise FileSystemError(
                f"Failed to traverse extracted directory: {error}") from error

        provided = set()
        for so_file in so_files:
            logging.debug("    -> Analyzing symbols from library: %s",
                          os.path.basename(so_file))
            provided.update(parse_symbols_from_readelf(so_file, 'provided'))

        return provided


def get_provided_symbols_from_rpm(rpm_url, rpm_cache_dir, session,
                                  symbol_cache=None):
    """从RPM包中提取提供的符号"""
    if symbol_cache is None:
        symbol_cache = {}

    rpm_filename = Path(rpm_url).name
    rpm_path = rpm_cache_dir / rpm_filename

    # 检查缓存
    if rpm_url in symbol_cache:
        logging.debug("  -> Using cached symbols for RPM: %s", rpm_filename)
        return symbol_cache[rpm_url]

    try:
        logging.debug("  -> Analyzing RPM: %s", rpm_filename)

        # 下载RPM文件 (如果缓存中不存在)
        if not rpm_path.exists():
            logging.debug(
                "    -> RPM not in cache. Downloading from %s", rpm_url)
            download_file_with_progress(session, rpm_url, rpm_path)
        else:
            logging.debug("    -> Found RPM in cache: %s", rpm_path)

        # 提取并分析
        provided = _extract_and_analyze_rpm(rpm_path, rpm_filename)

        # 缓存符号结果
        symbol_cache[rpm_url] = provided
        return provided

    except (NetworkError, FileSystemError, ExternalCommandError) as error:
        logging.warning(
            "  -> Warning: Failed to process RPM %s: %s", rpm_url, error)
        symbol_cache[rpm_url] = set()
        return set()


def get_provided_symbols_from_rpms(rpm_urls, rpm_cache_dir):
    """批量处理多个RPM包，避免重复下载"""
    symbol_cache = {}
    all_provided_symbols = set()

    unique_rpm_urls = set(rpm_urls)
    logging.debug("  -> Processing %d unique RPM packages",
                  len(unique_rpm_urls))

    with requests.Session() as session:
        for rpm_url in unique_rpm_urls:
            provided = get_provided_symbols_from_rpm(
                rpm_url, rpm_cache_dir, session, symbol_cache)
            all_provided_symbols.update(provided)

    return all_provided_symbols


def analyze_library_compatibility(dependencies, package_db, library_db):
    """分析库兼容性，返回匹配和不匹配的库信息"""
    mismatched_libs_report = []
    rpms_for_symbol_check = set()

    # 处理系统库
    for lib in sorted(dependencies['system']):
        if lib in library_db:
            provider_pkg = library_db[lib]
            rpms_for_symbol_check.add(provider_pkg)
            logging.debug("✓ Exact match: %s -> %s", lib, provider_pkg)
        else:
            base_name = lib.split('.so')[0]
            alternatives = [lib_name for lib_name in library_db.keys()
                            if lib_name.startswith(base_name)]

            if alternatives:
                # 找到相似库，但版本不匹配
                providers = {library_db[alt] for alt in alternatives}
                best_provider = sorted(providers)[0]
                provider_info = f"{best_provider}-{package_db[best_provider]['version']}"
                notes = f"OC9 提供不兼容版本: {', '.join(sorted(alternatives))}"
                mismatched_libs_report.append(
                    [lib, "❌ 不兼容", provider_info, notes])
            else:
                mismatched_libs_report.append(
                    [lib, "❌ 未找到", "N/A", "在OC9仓库中未找到任何相关包"])

    # 处理 not_found 库
    for lib in sorted(dependencies['not_found']):
        mismatched_libs_report.append(
            [lib, "❌ 未找到 (ldd)", "N/A", "ldd无法在当前系统找到此库"])

    return mismatched_libs_report, rpms_for_symbol_check


def _get_function_name(symbol):
    """提取符号的基础函数名，去除版本信息"""
    return symbol.split('@')[0]


def _symbol_version_matches(required_symbol, provided_symbols):
    """
    检查所需符号是否在提供的符号集合中有匹配版本
    """
    if required_symbol in provided_symbols:
        return True

    if 'GLIBC_PRIVATE' in required_symbol:
        return False

    required_function = _get_function_name(required_symbol)

    for provided_symbol in provided_symbols:
        provided_function = _get_function_name(provided_symbol)
        if required_function == provided_function:
            return True

    return False


def perform_symbol_check(binary_path, rpms_for_symbol_check, location_db,
                         rpm_cache_dir):
    """执行符号检查"""
    logging.debug("Stage 3: Symbol-Level Compatibility Check (checking %d matched libraries)",
                  len(rpms_for_symbol_check))
    logging.debug("Matched packages: %s", rpms_for_symbol_check)

    required_symbols = get_required_symbols(binary_path)
    rpm_urls_to_check = [
        location_db[pkg_name] for pkg_name in rpms_for_symbol_check
        if pkg_name in location_db
    ]

    if rpm_urls_to_check:
        provided_symbols = get_provided_symbols_from_rpms(rpm_urls_to_check,
                                                          rpm_cache_dir)

        missing_symbols = []
        for required_symbol in required_symbols:
            if not _symbol_version_matches(required_symbol, provided_symbols):
                missing_symbols.append(required_symbol)

        return sorted(missing_symbols)

    return []
